package itsec1.customermanagement;


import itsec1.api.services.AllowedRequests;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.Signature;
import java.security.SignatureException;
import java.sql.Blob;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Arrays;
import java.util.Hashtable;

public class UserDAO {
	// facade method checks whether a user with that name already exists. if not
	// -> call real registerUser method
	public static boolean registerUser(CustomerManagement cm, String username,
			String passwordText, String allowedRequestsString)
			throws IOException {
		Connection conn = cm.getDBConnection();
		boolean success = false;

		String sql = "SELECT username FROM serviceuser WHERE username=?";

		try {
			PreparedStatement pstmt = conn.prepareStatement(sql);
			pstmt.setString(1, username);

			ResultSet rs = pstmt.executeQuery();

			// if user with same name was not found -> request ok
			if (!rs.next())
				success = true;
		} catch (SQLException e) {
			System.out.println("SQLException: " + e.getMessage());
			System.out.println("SQLState: " + e.getSQLState());
			System.out.println("VendorError: " + e.getErrorCode());
			e.printStackTrace();
		}

		// close connection
		try {
			if (conn != null && !conn.isClosed())
				conn.close();
		} catch (SQLException e) {
		}

		if (success)
			return registerUser(cm, username, passwordText,
					new AllowedRequests(allowedRequestsString));
		else
			return false;
	}

	// insert service user with encrypted data into db
	public static boolean registerUser(CustomerManagement cm, String username,
			String passwordText, AllowedRequests allowedRequestsObject)
			throws IOException {
		Connection conn = cm.getDBConnection();
		Signature signature = cm.getSignature();
		String allowedRequests = allowedRequestsObject.toString();
		String blocked = "N";
		boolean success = false;

		// @formatter:off
		String sql= "INSERT INTO serviceuser SET " +
				"username=?," +
				"password=?," +
				"allowedrequests=?," +
				"blocked=?," +
				"signature=?";
		// @formatter:on

		try {
			byte[] passwordBytes= cm.encrypt(HashUtil.encodePassword(passwordText));
			byte[] allowedRequestsBytes= cm.encrypt(allowedRequests);
			byte[] blockedBytes= cm.encrypt(blocked);
			
			PreparedStatement pstmt = conn.prepareStatement(sql);
			pstmt.setString(1, username);
			pstmt.setBytes(2, passwordBytes);
			pstmt.setBytes(3, allowedRequestsBytes);
			pstmt.setBytes(4, blockedBytes);

			// sign data
			signature.initSign(cm.getKeyPair().getPrivate());
			signature.update(username.getBytes());
			signature.update(passwordBytes);
			signature.update(allowedRequestsBytes);
			signature.update(blockedBytes);

			pstmt.setBytes(5, signature.sign());

			pstmt.execute();
			pstmt.close();

			success = true;
		} catch (InvalidKeyException e) {
			e.printStackTrace();
		} catch (SignatureException e) {
			e.printStackTrace();
		} catch (SQLException e) {
			System.out.println("SQLException: " + e.getMessage());
			System.out.println("SQLState: " + e.getSQLState());
			System.out.println("VendorError: " + e.getErrorCode());
			e.printStackTrace();
		}

		// close connection
		try {
			if (conn != null && !conn.isClosed())
				conn.close();
		} catch (SQLException e) {
		}

		return success;
	}

	// delete/block service user
	public static boolean unregisterUser(CustomerManagement cm, String username)
			throws IOException {
		//first verify whether data in db wasn't manipulated by checking signature
		//if data doesn't fit to signature -> request denied
		if(!verifyUserData(cm,username))
			return false;
				
		Connection conn = cm.getDBConnection();
		boolean success = false;

		String sql = "UPDATE serviceuser SET blocked=?, signature=? WHERE username=?";

		try {
			byte[] blocked = cm.encrypt("Y");
			byte[] newSignature= getUpdatedDataSignature(cm,username,"blocked",blocked);

			PreparedStatement pstmt = conn.prepareStatement(sql);
			pstmt.setBinaryStream(1, new ByteArrayInputStream(blocked),
					blocked.length);
			pstmt.setBinaryStream(2, new ByteArrayInputStream(newSignature),
					newSignature.length);
			pstmt.setString(3, username);

			pstmt.execute();

			if (pstmt.getUpdateCount() > 0)
				success = true;
			pstmt.close();

		} catch (SQLException e) {
			System.out.println("SQLException: " + e.getMessage());
			System.out.println("SQLState: " + e.getSQLState());
			System.out.println("VendorError: " + e.getErrorCode());
			e.printStackTrace();
		}

		// close connection
		try {
			if (conn != null && !conn.isClosed())
				conn.close();
		} catch (SQLException e) {
		}

		return success;
	}

	// delete user (only for testing purposes)
	public static void deleteUser(CustomerManagement cm, String username)
			throws IOException {		
		Connection conn = cm.getDBConnection();

		String sql = "DELETE FROM serviceuser WHERE username=?";

		try {
			PreparedStatement pstmt = conn.prepareStatement(sql);
			pstmt.setString(1, username);

			pstmt.execute();
			pstmt.close();
		} catch (SQLException e) {
			System.out.println("SQLException: " + e.getMessage());
			System.out.println("SQLState: " + e.getSQLState());
			System.out.println("VendorError: " + e.getErrorCode());
			e.printStackTrace();
		}

		// close connection
		try {
			if (conn != null && !conn.isClosed())
				conn.close();
		} catch (SQLException e) {
		}
	}

	public static AllowedRequests authenticateUser(CustomerManagement cm,
			String username, String password) {
		//first verify whether data in db wasn't manipulated by checking signature
		//if data doesn't fit to signature -> request denied
		if(!verifyUserData(cm,username))
			return null;
				
		Connection conn = cm.getDBConnection();
		String dbUsername = null;
		byte[] dbPasswordHash = null;
		String dbBlocked = null;
		boolean authenticated = false;
		byte[] passwordHash = HashUtil.encodePassword(password);

		String sql = "SELECT username,password,allowedrequests,blocked FROM serviceuser WHERE username=?";

		try {
			PreparedStatement pstmt = conn.prepareStatement(sql);
			pstmt.setString(1, username);

			ResultSet rs = pstmt.executeQuery();
			int count = 0;
			AllowedRequests allowed = null;

			while (rs.next()) {
				count++;
				dbUsername = rs.getString("username");
				Blob passwordBlob = rs.getBlob("password");
				Blob allowedRequestsBlob = rs.getBlob("allowedrequests");
				Blob blockedBlob = rs.getBlob("blocked");

				if (passwordBlob != null) {
					dbPasswordHash = cm.decryptAsBytes(passwordBlob.getBytes(1,
							(int) passwordBlob.length()));
					dbBlocked = cm.decrypt(blockedBlob.getBytes(1,
							(int) blockedBlob.length()));

					// decrypt and deserialize AllowedRequests object
					allowed = new AllowedRequests(
							cm.decrypt(allowedRequestsBlob.getBytes(1,
									(int) allowedRequestsBlob.length())));
					
					Hashtable<String, Integer> invalidLogins= cm.getInvalidLoginTries();

					// check authentification
					if (dbUsername.equals(username)
							&& Arrays.equals(dbPasswordHash, passwordHash)
							&& dbBlocked.equals("N"))
					{
						authenticated = true;
						
						//reset login tries
						invalidLogins.remove(username);
					}else if(dbUsername.equals(username) && dbBlocked.equals("N")){						
						if(invalidLogins.containsKey(username)){
							int tries= invalidLogins.get(username) + 1;
							
							//if 4 invalid tries have been reached -> block user
							if(tries >= 4)
							{
								unregisterUser(cm,username);
								invalidLogins.remove(username);								
							}else
								invalidLogins.put(username, tries);
						}else
							invalidLogins.put(username, 1);
					}
				}
			}

			if (count == 1 && authenticated && allowed != null) {
				return allowed;
			} else {
				// more than one (should not occur) or no customer with id found
				return null;
			}
		} catch (SQLException e) {
			System.out.println("SQLException: " + e.getMessage());
			System.out.println("SQLState: " + e.getSQLState());
			System.out.println("VendorError: " + e.getErrorCode());
			e.printStackTrace();

			return null;
		} catch (Exception e) {
			e.printStackTrace();

			return null;
		}
	}
	
	private static boolean verifyUserData(CustomerManagement cm,String username){
		Connection conn = cm.getDBConnection();
		Signature signature=null;
		signature = cm.getSignature();
		try {
			signature.initVerify(cm.getKeyPair().getPublic());
		} catch (InvalidKeyException e1) {
			e1.printStackTrace();
		}
		
		boolean verified= false;
		
		// @formatter:off
		String sql= "SELECT username," +
				"password," +
				"allowedrequests," +
				"blocked," +
				"signature FROM serviceuser WHERE username=?";
		// @formatter:on
		
		try {
			PreparedStatement pstmt = conn.prepareStatement(sql);
			pstmt.setString(1, username);

			ResultSet rs = pstmt.executeQuery();

			if (rs.next()) {
				signature.update(username.getBytes());
				for(int i=2; i <= 4; i++){
					Blob resultBlob = rs.getBlob(i);
					signature.update(resultBlob.getBytes(1,
							(int) resultBlob.length()));				
				}
				Blob signatureBlob= rs.getBlob("signature");
				
				verified= signature.verify(signatureBlob.getBytes(1,
						(int) signatureBlob.length()));
			}
		} catch (SQLException e) {
			System.out.println("SQLException: " + e.getMessage());
			System.out.println("SQLState: " + e.getSQLState());
			System.out.println("VendorError: " + e.getErrorCode());
			e.printStackTrace();
		} catch (SignatureException e) {
			e.printStackTrace();
		}
		
		// close connection
		try {
			if (conn != null && !conn.isClosed())
				conn.close();
		} catch (SQLException e) {		}
		
		return verified;
	}
	
	private static byte[] getUpdatedDataSignature(CustomerManagement cm,String username,String field,byte[] value) throws IOException{
		Connection conn = cm.getDBConnection();
		Signature signature=null;
		byte[] sig= null;
				
		signature = cm.getSignature();
		try {
			signature.initSign(cm.getKeyPair().getPrivate());
		} catch (InvalidKeyException e1) {
			e1.printStackTrace();
		}
		
		// @formatter:off
		String sql= "SELECT username," +
		"password," +
		"allowedrequests," +
		"blocked," +
		"signature FROM serviceuser WHERE username=?";
		// @formatter:on
		
		try {
			PreparedStatement pstmt = conn.prepareStatement(sql);
			pstmt.setString(1, username);

			ResultSet rs = pstmt.executeQuery();

			if (rs.next()) {
				signature.update(username.getBytes());
				int updateField= rs.findColumn(field);					
				
				//iterate over all blob fields and add values to signature calculation
				for(int i=2; i <= 4; i++){
					//if data of field is not the updated one -> take value from select
					if(i != updateField){
						Blob resultBlob = rs.getBlob(i);
						signature.update(resultBlob.getBytes(1,
								(int) resultBlob.length()));
					}else{
						//take new (encrypted) value of the field
						signature.update(value);
					}
				}
				sig= signature.sign();
			}
		} catch (SQLException e) {
			System.out.println("SQLException: " + e.getMessage());
			System.out.println("SQLState: " + e.getSQLState());
			System.out.println("VendorError: " + e.getErrorCode());
			e.printStackTrace();
		} catch (SignatureException e) {
			e.printStackTrace();
		}
		
		// close connection
		try {
			if (conn != null && !conn.isClosed())
				conn.close();
		} catch (SQLException e) {		}
		
		return sig;
	}
}
